在函數式編程中,我們總是希望能寫出更富表達能力的函式,儘可能簡化每個 function body 需要處理的內容。Guards 讓你可以在函式的第一行,快速處理一些常用的條件判別。搭配上之前所學到的 function pattern matching,能讓程式更易於閱讀與維護。我們先看一個簡單的例子:
defmodule Math do
def abs(i) when i < 0, do: -i
def abs(i), do: i
end
Guards 就是在函式宣告的參數括號後方,加上 when
及一個回傳布林值的 expression。在這個例子中,我們在第一個函式區塊判斷傳入的 i
是否小於 1
。而當 guards 判別回傳 false
時,就會向下繼續尋找可用的函式區塊。
由於有編譯及執行速度上的考量,guards 只能用限定的函式及操作符,包括四則運算、大小判斷如 ==
、>
、<
,型別判斷如 is_list
,is_boolean
等,更詳細的清單請參考官網的詳細列表。
特別要注意的是在 guards 裡不能呼叫自定義的一般函式。目前若需要自定義 guard,需要用進階的 defmacro
,並在該 macro 中只用到上述清單中允許的函式。而且除了檢查並回傳布林值之外,不能有其它的副作用。而新版的 elixir 中也將會提供 defguard
用來自訂義 guard,並在編譯期進行檢查。
Guards 可以用在三個地方,分別是具名函式及匿名函式宣告,及 case
表達式裡,會在相應的介紹裡說明。
若要在同個函式區塊中使用多個 guards,要用 and
或是 or
。 and
就是必須符合所有的條件,而 or
則是只要任一條件符合即可。
def is_even(n) when is_integer(n) and rem(n, 2) == 0,
do: true
def is_even(_other),
do: false
def is_number(term)
when is_integer(term)
or is_float(term),
do: true
def is_number(_other),
do: false
另外依程式可讀性需求,你也可以把 or
改成 when
。
def is_number(term)
when is_integer(term)
when is_float(term),
do: true
def is_number(_other),
do: false
|>
不管是寫哪種程式語言,只看程式的形狀,你一定做過類似這樣的事情:
request = generate_request
response = get_response(request)
body = parse_body(response, :html)
html = render(body)
但仔細想想,當你最終想要的只有那個 html
的結果,那這個例子中的 request
、response
跟 body
都是沒有用的臨時變數。想要去掉這些變數,你可以這樣寫:
html = render(parse_body(get_response(generate_request), :html))
然後你就被同事記恨了。因為這看起來更醜,得要從裡面讀到外面,而且還常常找不到開始讀的點。大概只有寫 LISP 的人才看得習慣。
Elixir 的 pipe operator |>
讓你把 expression 的結果當成下一個函式呼叫的 第一個參數,所以上面的例子可以改寫成:
html =
generate_request
|> get_response
|> parse_body(:html)
|> render
在數學中,這個行為叫做 function composition,用 Haskell 為例,當你想要得到以下式子的結果:
y = f(g(h(x)))
你想要的,就是 f, g 跟 h 的複合函數,在 Haskell 中,你可以這樣寫:
y = f.g.h x
但雖然省掉了臨時變數跟括號, function composition 的寫法依然是由內而外的。Elixir 的 pipe operator 則是符合執行順序的:
y = x |> h |> g |> f
著名的 JavaScript FP 函式庫 Ramda 及 Lodash/fp 都提供了兩種方向的函式。Ramda 裡是 R.compose
及 R.pipe
, 在 lodash/fp 裡叫做 fp.compose
及 fp.pipe
。
記得我們之前為了取得執行的結果,做了這樣的事:
y = Math.add_one(10)
IO.inspect(y)
你可以把該例子改成這樣:
Math.add_one(10)
|> IO.inspect
而 IO.inspect
不同於專用於字串列印的 IO.puts
,它會將接收到的參數印出來,並將結果繼續向下傳。所以你還可以這樣做:
html =
generate_request
|> IO.inspect
|> get_response
|> IO.inspect
|> parse_body(:html)
# |> IO.inspect
|> render
|> IO.inspect
不想印某些結果時,只要像上面把該行註解掉就好。
Pipe operator 是 Elixir 裡最著名也最重要的操作符,請務必要熟悉它的用法。
when
幫函式加上 guards ,直接對值進行檢查case
表達式中|>
會將表達式的結果,當做下一個函式呼叫的第一個參數
IO.inspect
會列印及回傳接收到的參數Happy hacking! 明天見。
文章中第一個範例的 defmodule
的尾巴是不是少了個 do
。 qq
defmodule Math
def abs(i) when i < 0, do: -i
def abs(i), do: i
end
啊,是的。感謝修正。